/**
 * Created with IntelliJ IDEA.
 * User: zzl
 * Date: 12-12-29
 * To change this template use File | Settings | File Templates.
 */
(function ($, undefined) {

    var $table;
    var $tbody;

    //
    var Action = function (config) {
        this.config = config;
    };
    Action.prototype = {
        constructor: Action,

        getSelectionAndActiveState: function() {
            var selectedRowIndexes = [];
            var $allRows = $tbody.children();
            var selectedRows = todolist.getSelectedRows();
            for(var n = 0; n < selectedRows.length; n++) {
                selectedRowIndexes.push(jsex.indexOf($allRows, selectedRows[n]));
            }
            var activeRowIndex = jsex.indexOf($allRows, todolist.getActiveRow());
            return {
                selectedRowIndexes: selectedRowIndexes,
                activeRowIndex: activeRowIndex
            }
        },
        restoreSelectionAndActiveState: function(state) {
            // optimize for compounded actions?
            var selectedRowIndexes = state.selectedRowIndexes;
            var $allRows = $tbody.children();
            var selectedRows = [];
            for(var n = 0; n < selectedRowIndexes.length; n++) {
                var index = selectedRowIndexes[n];
                selectedRows.push($allRows[index]);
            }
            todolist.select(selectedRows);
            var activeRow = $allRows[state.activeRowIndex];
            todolist.setActiveRow(activeRow, true);
        }
    };

    /**
     * config:
     *   editable: element
     *   oriText
     *   newText
     */
    var EditAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
    };
    EditAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: EditAction,
        updateText: function (text) {
            var me = this;
            var $el = $(me.config.editable);
            var $row = $(me.config.editable).parents('tr:first');
            if (!$table.triggerHandler('setTextRequest', [$el[0], text])) {
                var html = undecorated.plainTextToHtml(text, true);
                $el.html(html);
            }
            todolist.setActiveRow($row);
            $el.focus();
        },
        undo: function () {
            var me = this;
            me.updateText(me.config.oriText);
        },
        redo: function () {
            var me = this;
            me.updateText(me.config.newText);
        }
    });

    /**
     * config:
     *   nodes: elements
     *   oriChecked: bool|bool[]
     *   newChecked: bool
     * construct:
     *   after action
     */
    var CheckAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.selectionAndActiveState = me.getSelectionAndActiveState();
    };
    CheckAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: CheckAction,
        updateChecked: function (checkedStates) {
            var me = this;
            var nodes = me.config.nodes;
            var isArray = $.isArray(checkedStates);
            for(var n = 0; n < nodes.length; n++) {
                var $node = $(nodes[n]);
                var  checked = isArray? checkedStates[n]: checkedStates;
                $node.toggleClass('checked', checked);
                $node.find(':checkbox').attr('checked', checked);
            }
            me.restoreSelectionAndActiveState(me.selectionAndActiveState);
        },
        undo: function () {
            var me = this;
            me.updateChecked(me.config.oriChecked);
        },
        redo: function () {
            var me = this;
            me.updateChecked(me.config.newChecked);
        }
    });

    /**
     *  config:
     *    nodes: elements
     *    cssClass: css class name
     *    oriState: ori hasClass state array
     *    newState: new hasClass state
     *  construct:
     *    after action
     */
    var CssClassAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.selectionAndActiveState = me.getSelectionAndActiveState();
    };
    CssClassAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: CssClassAction,
        updateClass: function (states) {
            var me = this;
            var nodes = me.config.nodes;
            var isArray = $.isArray(states);
            for (var n = 0; n < nodes.length; n++) {
                var node = nodes[n];
                var state = isArray? states[n]: states;
                $(node).toggleClass(me.config.cssClass, state);
            }
            me.restoreSelectionAndActiveState(me.selectionAndActiveState);
        },
        undo: function () {
            var me = this;
            me.updateClass(me.config.oriState);
        },
        redo: function () {
            var me = this;
            me.updateClass(me.config.newState);
        }
    });

    /**
     *  config:
     *    nodes: elements
     *    dataName: data name
     *    oriData: ori data array
     *    newData: new data
     *  construct:
     *    after action
     */
    var DataAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.selectionAndActiveState = me.getSelectionAndActiveState();
    };
    DataAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: DataAction,
        updateData: function (datas) {
            var me = this;
            var nodes = me.config.nodes;
            for (var n = 0; n < nodes.length; n++) {
                var $node = $(nodes[n]);
                var data = datas.length ? datas[n] : datas;
                if (data === undefined) {
                    $node.removeData(me.config.dataName);
                }
                else {
                    $node.data(me.config.dataName, data);
                }
                $node.trigger('updateNodeDecorationsRequest');
            }
            me.restoreSelectionAndActiveState(me.selectionAndActiveState);
        },
        undo: function () {
            var me = this;
            me.updateData(me.config.oriData);
        },
        redo: function () {
            var me = this;
            me.updateData(me.config.newData);
        }
    });

    /**
     *  config:
     *    nodes: elements
     *    attrName: attr name
     *    oriAttr: ori attr array
     *    newAttr: new attr
     *  construct:
     *    after action
     */
    var AttrAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.selectionAndActiveState = me.getSelectionAndActiveState();
    };
    AttrAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: AttrAction,
        updateAttr: function (attrs) {
            var me = this;
            var nodes = me.config.nodes;
            var isArray= $.isArray(attrs);
            for (var n = 0; n < nodes.length; n++) {
                var $node = $(nodes[n]);
                var attr = isArray ? attrs[n] : attrs;
                if (attr === undefined) {
                    $node.removeAttr(me.config.attrName);
                }
                else {
                    $node.attr(me.config.attrName, attr);
                }
                $node.trigger('updateNodeDecorationsRequest');
            }
            me.restoreSelectionAndActiveState(me.selectionAndActiveState);
        },
        undo: function () {
            var me = this;
            me.updateAttr(me.config.oriAttr);
        },
        redo: function () {
            var me = this;
            me.updateAttr(me.config.newAttr);
        }
    });

    /**
     *  config:
     *    na
     *  properties:
     *    nodes
     *  construct:
     *    before action
     *  call:
     *    after action
     *
     */
    var NewAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.selectionAndActiveStateBefore = me.getSelectionAndActiveState();
    };
    NewAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: NewAction,
        afterAction: function () {
            var me = this;
            console.log('*************** NewAction ' + me.nodes.length);
            var $allRows = $tbody.children();
            me.rows = [];
            for (var n = 0; n < me.nodes.length; n++) {
                me.rows.push($.inArray(me.nodes[n], $allRows));
            }
            me.parentNode = todolist.getParentNode(me.nodes[0]);
            me.selectionAndActiveStateAfter = me.getSelectionAndActiveState();
        },
        undo: function () {
            var me = this;
            console.log('*************** NewAction undo ' + me.nodes.length);

            var treelineRelatedNodes = todolist.collectTreelineRelatedNodes(me.nodes);
            $(me.nodes).remove();
            todolist.updateChildrenAndExpand(me.parentNode);
            todolist.updateIndentLines(treelineRelatedNodes);
            me.restoreSelectionAndActiveState(me.selectionAndActiveStateBefore);

            //ttt
            $table.trigger('rowsChanged');
        },
        redo: function () {
            var me = this;

            var $dummyRow = $('<tr><td></td><td></td><td></td><td></td></tr>');
            $tbody.append($dummyRow);
            var $allRows = $tbody.children();
            var n, node;
            for (n = 0; n < me.nodes.length; n++) {
                node = me.nodes[n];
                var row = me.rows[n];
                $(node).insertBefore($allRows[row]);
                $allRows.splice(row, 0, node);
            }
            $dummyRow.remove();

            todolist.updateChildrenAndExpand(me.parentNode);
            todolist.updateIndentLinesExtended(me.nodes);
            me.restoreSelectionAndActiveState(me.selectionAndActiveStateAfter);

            //ttt
            $table.trigger('rowsChanged');
        }
    });

    /**
     *  config:
     *    nodes: sorted by index
     *  construct:
     *    before action
     *  call:
     *    after action
     *
     */
    var DeleteAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.selectionAndActiveStateBefore = me.getSelectionAndActiveState();

        me.nodeAndDescendantsArr = [];
        me.rows = [];
        var $allRows = $tbody.children();
        var nodes = me.config.nodes;

        var parentNodes = [];

        for (var n = 0; n < nodes.length; n++) {
            var node = nodes[n];
            var nodeAndDescendants = [node];
            todolist.collectDescendants(node, nodeAndDescendants);
            me.nodeAndDescendantsArr.push(nodeAndDescendants);
            me.rows.push($.inArray(node, $allRows));

            var parentNode = todolist.getParentNode(nodes[n]);
            parentNodes.push(parentNode);
        }
        me.parentNodes = jsex.distinct(parentNodes);
    };
    DeleteAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: DeleteAction,
        afterAction: function() {
            var me = this;
            me.selectionAndActiveStateAfter = me.getSelectionAndActiveState();
        },
        undo: function () {
            var me = this;

            var $dummyRow = $('<tr><td></td><td></td><td></td><td></td></tr>');
            $tbody.append($dummyRow);
            var $allRows = $tbody.children();
            var n, node;
            var allDeletedNodes = [];
            var nodes = me.parentNodes;
            for (n = 0; n < nodes.length; n++) {
                todolist.expandNode(nodes[n]);
            }
            nodes = me.config.nodes;
            for (n = 0; n < nodes.length; n++) {
                node = nodes[n];
                var row = me.rows[n];
                var nodeAndDescendants = me.nodeAndDescendantsArr[n];
                allDeletedNodes = allDeletedNodes.concat(nodeAndDescendants);
                $(nodeAndDescendants).insertBefore($allRows[row]);
                Array.prototype.splice.apply($allRows, [row, 0].concat(nodeAndDescendants));
            }
            $table.trigger('nodeAdded', [allDeletedNodes]);
            $dummyRow.remove();

            todolist.updateChildrenAndExpand(todolist.getRootNode());
            todolist.updateNodeVisibility(me.parentNodes);

            todolist.updateIndentLinesExtended(nodes);

            me.restoreSelectionAndActiveState(me.selectionAndActiveStateBefore);


            //ttt
            $table.trigger('rowsChanged');
        },
        redo: function () {
            var me = this;
            var nodes = me.config.nodes;
            var parentNodes = [];
            var treelineRelatedNodes = todolist.collectTreelineRelatedNodes(nodes);
            for (var n = 0; n < me.nodeAndDescendantsArr.length; n++) {
                var node = nodes[n];
                var parentNode = todolist.getParentNode(node);
                parentNodes.push(parentNode);
                $(me.nodeAndDescendantsArr[n]).remove();
            }
            parentNodes = jsex.distinct(parentNodes);
            for(var n = 0; n < parentNodes.length; n++) {
                todolist.updateChildrenAndExpand(parentNodes[n]);
            }
            todolist.updateIndentLines(treelineRelatedNodes);
            me.restoreSelectionAndActiveState(me.selectionAndActiveStateAfter);

            //ttt
            $table.trigger('rowsChanged');
        }
    });

    /**
     *  config:
     *    nodes: separated branch and sorted
     *  construct:
     *    before action
     *  call:
     *    after action
     *
     */
    var TreeAction = function (config) {
        var me = this;
        Action.apply(me, arguments);

        var $allRows = $tbody.children();
        me.rowIndexesBefore = [];
        me.levelsBefore = [];
        me.nodeAndDescendantsArr = [];
        var nodes = me.config.nodes;
        for (var n = 0; n < nodes.length; n++) {
            var node = nodes[n];
            me.rowIndexesBefore.push($.inArray(node, $allRows));
            me.levelsBefore.push($(node).data('level'));
            me.nodeAndDescendantsArr.push(todolist.collectDescendants(node, [node]));
        }
        me.fromParentNodes = todolist.getParentNodes(nodes, true);
        me.selectionAndActiveStateBefore = me.getSelectionAndActiveState();
    };
    TreeAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: TreeAction,
        afterAction: function () {
            var me = this;
            var $allRows = $tbody.children();
            me.rowIndexesAfter = [];
            me.levelsAfter = [];
            var nodes = me.config.nodes;
            for (var n = 0; n < nodes.length; n++) {
                var node = nodes[n];
                me.rowIndexesAfter.push($.inArray(node, $allRows));
                me.levelsAfter.push(parseInt($(node).data('level')));
            }
            me.toParentNodes = todolist.getParentNodes(nodes, true);
            me.parentNodes = jsex.union(me.fromParentNodes, me.toParentNodes); //keep rowIndex only?
            me.fromParentNodes = me.toParentNodes = undefined;
            me.selectionAndActiveStateAfter = me.getSelectionAndActiveState();
        },
        applyChange: function (rowIndexes, fromLevels, toLevels, selectionAndActiveState) {
            var me = this;
            var count = rowIndexes.length;
            var n;
            var nodes = me.config.nodes;

            var treeLineRelatedNodes = [];
            todolist.collectTreelineRelatedNodes(nodes, treeLineRelatedNodes)
            for (n = 0; n < count; n++) {
                $(me.nodeAndDescendantsArr[n]).remove();
            }
            for (n = 0; n < count; n++) {
                var levelDiff = toLevels[n] - fromLevels[n];

                var node = nodes[n];

                var rowIndex = rowIndexes[n];
                var $relNode = $tbody.children(':nth-child(' + (rowIndex) + ')');
                if ($relNode.length) {
                    $relNode.after(me.nodeAndDescendantsArr[n]);
                }
                else {
                    $relNode = $tbody.children(':nth-child(' + (rowIndex + 1) + ')');
                    console.assert($relNode.length);
                    $relNode.before(me.nodeAndDescendantsArr[n]);
                }
//                $table.trigger('nodeAdded', [me.nodeAndDescendantsArr[n]]);
//                $table.treeTable('buildChildrenData', me.nodeAndDescendantsArr[n]);
                todolist.setupNodeRelations(me.nodeAndDescendantsArr[n]);
                todolist.updateBranchLevel(node, levelDiff);
            }
            for (n = 0; n < me.parentNodes.length; n++) {
                todolist.updateChildrenAndExpand(me.parentNodes[n]);
            }

            todolist.collectTreelineRelatedNodes(nodes, treeLineRelatedNodes)
            treeLineRelatedNodes = jsex.union(treeLineRelatedNodes, nodes);
            todolist.updateIndentLines(treeLineRelatedNodes);

            me.restoreSelectionAndActiveState(selectionAndActiveState);

            //ttt
            $table.trigger('rowsChanged');
        },
        undo: function () {
            var me = this;
            me.applyChange(me.rowIndexesBefore, me.levelsAfter, me.levelsBefore,
                me.selectionAndActiveStateBefore);
        },
        redo: function () {
            var me = this;
            me.applyChange(me.rowIndexesAfter, me.levelsBefore, me.levelsAfter,
                me.selectionAndActiveStateAfter);
        }
    });

    //
    var CompoundAction = function (config) {
        var me = this;
        Action.apply(me, arguments);
        me.actions = [];
    };
    CompoundAction.prototype = $.extend(jsex.newRawObj(Action), {
        constructor: CompoundAction,
        beforeAction: function () {

        },
        afterAction: function () {

        },
        undo: function () {
            var me = this;
            for (var n = me.actions.length - 1; n >= 0; --n) {
                me.actions[n].undo();
            }
        },
        redo: function () {
            var me = this;
            for (var n = 0; n < me.actions.length; n++) {
                me.actions[n].redo();
            }
        }
    });

    /**/
    var undoman = {
    };
    undoman.setTable = function (table) {
        $table = $(table);
        $tbody = $table.children('tbody.nodes');
    };

    var actionMap = {
        edit: EditAction,
        check: CheckAction,
        cssClass: CssClassAction,
        data: DataAction,
        attr: AttrAction,
        'new': NewAction,
        'delete': DeleteAction,
        tree: TreeAction
    };

    var actions = [];
    var pointer = 0;

    undoman.newAction = function (config) {
        var action = new actionMap[config.action](config);
        return action;
    };

    undoman.onAction = function (actionOrConfig) {
        var me = this;
        var action;
        if ('action' in actionOrConfig) {
            action = new actionMap[actionOrConfig.action](actionOrConfig);
        }
        else {
            action = actionOrConfig;
        }
        if (me.activeCompoundAction) {
            me.activeCompoundAction.actions.push(action);
        }
        else {
            actions.splice(pointer, actions.length - pointer);
            actions.push(action);
            pointer = actions.length;
        }
        return action;
    };

    undoman.canUndo = function () {
        return pointer > 0;
    };

    undoman.canRedo = function () {
        return pointer < actions.length;
    };

    undoman.undo = function () {
        console.assert(undoman.canUndo());
        var action = actions[--pointer];
        action.undo();
    };

    undoman.redo = function () {
        console.assert(undoman.canRedo());
        var action = actions[pointer++];
        action.redo();
    };

//    undoman.saveActiveStateBeforeAction = ActionFuncs.saveActiveStateBeforeAction;

    undoman.getLastAction = function () {
        if (pointer < 1) {
            return null;
        }
        return actions[pointer - 1];
    };

    undoman.beginCompoundAction = function () {
        var me = this;
        var action = new CompoundAction();
        action.beforeAction();
        me.onAction(action);
        me.activeCompoundAction = action;
    };

    undoman.endCompoundAction = function () {
        var me = this;
        undoman.getLastAction().afterAction();
        me.activeCompoundAction = null;
    };

    window.undoman = undoman;

})(jQuery);